//
//  ScriptEngine.swift
//  MacDemo
//
//  Created by FH on 2020/11/18.
//  Copyright © 2020 FH. All rights reserved.
//

import Foundation

typealias DispatchHandleFn = Action2<[String : Any], Bool>
typealias DispatchRespondsFn = (AnyObject) -> DispatchHandleFn?
typealias DispatchFilterFn = (String, Bool) -> (Bool, DispatchRespondsFn?)
typealias CustomSetInstancePropertiesHandler = (String, AnyObject, String, Any?) -> Bool

protocol ScriptEngineDispatch {
    func responseDelegate(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any], isSDKInstance: Bool, instanceClass: String)
    func responseBlock(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any], isSDKInstance: Bool, instanceClass: String)
    func responseAPI(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any], returnValue: Any, isSDKInstance: Bool, instanceClass: String)
}

@objc
class ScriptEngine : NSObject {
    // import MetaClass
    private static let NSInvocationClass = importClass("NSInvocation") as NSInvocationPrivate.Type
    private static let NSMethodSignatureClass = importClass("NSMethodSignature") as NSMethodSignaturePrivate.Type
    // invoke info
    private let astNodes: [String : AstNode]!
    private let apiMethods: [String : AstNode]!
    private let apiSelectorMethods: [Selector : AstNode]
    private let delegateMethods: [String : AstNode]!
    private let delegateSelectorMethods: [Selector : AstNode]
    private var blockMethods: [Selector : AstNode] = [:]
    private var classesInfo: [String : AnyClass] = [:]
    private var invokeTargetClassName: String = ""
    private var invokeTargetClass: AnyClass!
    private (set) var invokeTarget: AnyObject!
    private var dispatchTargets: [AnyObject] = []
    private var runInMainQueueTargets: NSMutableSet = NSMutableSet()
    private var dispatchFilterFnList: [DispatchFilterFn] = []
    private var handleCustomSetInstanceProperties: CustomSetInstancePropertiesHandler? = nil
    // other
    private let mutex = Mutex()
    private var engineName: String!
    private(set) var isRunning = false
    
    override var description: String {
        return "engine: \(self.engineName ?? "")"
    }
    deinit {
        print("TODO: remove for test")
    }
    init(astNodes: [String : AstNode], apiClassName: String, delegateClassNames: [String]) {
        // register main queue specific
        DispatchQueue.main.setSpecific(key: MainSpecificKey, value: MainSpecificValue)
        // force import NSMethodSignatureClass and NSMethodSignatureClass
        let _ = Self.NSMethodSignatureClass.self
        let _ = Self.NSInvocationClass.self
        // set property/param order
        self.astNodes = astNodes
        // get all methods of sdk apis
        var apiMethods = [String : AstNode]()
        var apiSelectorMethods = [Selector : AstNode]()
        if let apiClass = astNodes[apiClassName] {
            self.invokeTargetClassName = apiClassName
            self.invokeTargetClass = NSClassFromString(apiClassName)!
            self.classesInfo[apiClassName] = self.invokeTargetClass
            for method in apiClass.children where method.type == .method {
                let selector = NSSelectorFromString(method.name)
                apiMethods[method.name] = method
                apiSelectorMethods[selector] = method
            }
        }
        self.apiMethods = apiMethods
        self.apiSelectorMethods = apiSelectorMethods
        // get all methods of delegates
        var delegateMethods = [String : AstNode]()
        var delegateSelectorMethods = [Selector : AstNode]()
        for delegate in astNodes.values where delegate.type == .protocol {
            if delegateClassNames.count > 0, !delegateClassNames.contains(delegate.name) {
                continue
            }
            for method in delegate.children {
                let selector = NSSelectorFromString(method.name)
                delegateMethods[method.name] = method
                delegateSelectorMethods[selector] = method
            }
        }
        self.delegateMethods = delegateMethods
        self.delegateSelectorMethods = delegateSelectorMethods
    }
    
    func setEngineName(_ name: String) {
        self.engineName = name
    }
    
    func addTarget(target: AnyObject, runInMainQueue: Bool) {
        let succ = mutex.tryLock()
        let isExists = self.dispatchTargets.contains { item in
            if let itemHash = item.hash, let targetHash = target.hash {
                return itemHash == targetHash
            }
            return false
        }
        if !isExists {
            self.dispatchTargets.append(target)
            if runInMainQueue {
                self.runInMainQueueTargets.add(target)
            }
        }
        if succ {
            mutex.unlock()
        }
    }
    
    func removeTarget(target: AnyObject) {
        let succ = mutex.tryLock()
        self.dispatchTargets.removeAll { $0.isEqual(target) }
        self.runInMainQueueTargets.remove(target)
        if succ {
            mutex.unlock()
        }
    }
    
    func addDispatchFilter(_ filterFn: @escaping DispatchFilterFn) {
        self.dispatchFilterFnList.append(filterFn)
    }
    
    func running(with invokeInstance: AnyObject?=nil) {
        if !self.isRunning {
            if let invokeInstance = invokeInstance {
                self.invokeTarget = invokeInstance
            }
            self.isRunning = true
        }
    }
    
    func stop() {
        if self.isRunning {
            self.isRunning = false
            self.invokeTarget = nil
        }
    }
    
    func registerSetPropertiesHandler(_ handler: @escaping CustomSetInstancePropertiesHandler) {
        self.handleCustomSetInstanceProperties = handler
    }
    
    // REMARK: get enum description on runtime
    
    func getEnumDescription(_ anyValue: Any, enumType: String="", isEnumValue: Bool=false) -> String {
        if enumType.count > 0, let enumNode = self.astNodes[enumType], enumNode.type == .enumerate {
            var value = anyValue
            if isEnumValue {
                value = castEnumToNativeValue(value: anyValue, rawType: enumNode.declType!)
            }
            let valueStr = String(reflecting: value)
            for item in enumNode.children {
                if let itemValue = item.value?.value, itemValue == valueStr {
                    return "\(item.name)(\(itemValue))"
                }
            }
            // handle not found
            return "!!!Not Found Enum Defined: \(valueStr)"
        } else {
            let realType = String(reflecting: type(of: anyValue))
            if realType.starts(with: "__C.") {
                let enumType = realType.replacingOccurrences(of: "__C.", with: "")
                return getEnumDescription(anyValue, enumType: enumType, isEnumValue: true)
            }
        }
        return ""
    }
    
    func getEnumValue(_ enumTypeStr: String, value: Any) -> Any? {
        var enumType = enumTypeStr
        if enumTypeStr.starts(with: "__C.") {
            enumType =  enumTypeStr.replacingOccurrences(of: "__C.", with: "")
        }
        if let enumNode = self.astNodes[enumType], enumNode.type == .enumerate {
            return castEnumToNativeValue(value: value, rawType: enumNode.declType!)
        }
        return nil
    }
    
    func getEnumValue(_ enumType: String, byCaseIndex: Int) -> Any? {
        if let enumNode = self.astNodes[enumType], enumNode.type == .enumerate {
            for i in 0..<enumNode.children.count {
                if i == byCaseIndex {
                    let caseNode = enumNode.children[i]
                    if let itemValue = caseNode.value?.value {
                        if caseNode.declType! == "NSInteger" {
                            return Int(itemValue)
                        } else if caseNode.declType == "NSUInteger" {
                            return UInt(itemValue)
                        }
                    }
                }
            }
        }
        return nil
    }
    
    // @param anyValue: rawValue or enumValue
    func getEnumIndex(_ anyValue: Any, enumType: String) -> Int {
        var isNativeValue = true
        var enumClass = enumType
        let valueType = String(reflecting: type(of: anyValue))
        if valueType.starts(with: "__C.") {
            isNativeValue = false
            if enumType == "" {
                enumClass = valueType.replacingOccurrences(of: "__C.", with: "")
            }
        }
        if enumClass.count > 0, let enumNode = self.astNodes[enumClass], enumNode.type == .enumerate {
            var value = anyValue
            if !isNativeValue {
                value = castEnumToNativeValue(value: anyValue, rawType: enumNode.declType!)
            }
            for i in 0..<enumNode.children.count {
                let caseNode = enumNode.children[i]
                if let itemValue = caseNode.value?.value {
                    var result: Int = Int.min
                    var uResult: UInt = UInt.max
                    if let intValue = value as? Int {
                        result = intValue
                    } else if let uintValue = value as? UInt {
                        uResult = uintValue
                    } else if let strValue = value as? String, let intValue = Int(strValue) {
                        result = intValue
                    }
                    if let intItemValue = Int(itemValue), intItemValue == result {
                        return i
                    } else if let uintItemValue = UInt(itemValue), uintItemValue == uResult {
                        return i
                    }
                }
            }
        }
        return 0
    }
    
    // REMARK: create other class instance and set properties

    func createInstance(_ className: String?, _ isStaticMethod: Bool, _ initSelector: String?, _ parameters: [Any?]?) -> AnyObject {
        let classTypeName = className ?? self.invokeTargetClassName
        var classType: AnyClass!
        var methods: [String : AstNode] = [:]
        var instance: AnyObject!
        if let className = className {
            if let cls = self.classesInfo[className] {
                classType = cls
            } else {
                classType = NSClassFromString(className)!
                self.classesInfo[className] = classType
            }
            for node in self.astNodes[className]!.children where node.type == .method {
                methods[node.name] = node
            }
            instance = self.proxy.createInstance(from: classType) as AnyObject
        } else {
            classType = self.invokeTargetClass
            methods = self.apiMethods
            instance = self.invokeTarget ?? NSObject()
        }
        if let selector = initSelector {
            let (canResponds, result) = _invoke(methods: methods,
                                                targetClass: classType,
                                                target: instance,
                                                isStaticMethod: isStaticMethod,
                                                method: selector,
                                                parameters: parameters ?? [],
                                                callAPICtx: [:],
                                                isSDKInstance: classTypeName == self.invokeTargetClassName,
                                                instanceClass: classTypeName)
            if canResponds, let obj = result as? NSObject {
                if obj.className == classTypeName {
                    instance = result as AnyObject
                }
            }
        }
        return instance
    }
    
    func getValue(_ forKeyPath: String, _ instance: AnyObject, _ modelName: String) -> Any? {
        if let modelNode = self.findAstNode(by: modelName), modelNode.type == .interface {
            return getValue(forKeyPath, instance, modelNode)
        }
        return nil
    }
    
    func getValue(_ forKeyPath: String, _ instance: AnyObject, _ modelNode: AstNode) -> Any? {
        var propNode: AstNode! = nil
        let paths = forKeyPath.components(separatedBy: ".")
        for item in modelNode.children where item.type == .property {
            if item.name == paths[0] {
                propNode = item
                break
            }
        }
        return _getValue(paths, keyIndex: 0, instance, propNode: propNode)
    }
    
    fileprivate func _getValue(_ keys: [String], keyIndex: Int, _ instance: AnyObject, propNode: AstNode) -> Any? {
        if let objcInstance = instance as? NSObject, let propValue = propNode.value {
            let key = keys[keyIndex]
            let currInstance = objcInstance.value(forKey: key)
            var result: Any? = currInstance
            if !propValue.isObjCRefType {
                var propType = propNode.declType!
                if let enumNode = self.findAstNode(by: propType), enumNode.type == .enumerate {
                    propType = enumNode.declType!
                }
                if let objcInstance = currInstance as? NSObject {
                    result = castObjCToAnyValue(value: objcInstance, valueType: propType)
                }
            }
            if keyIndex + 1 == keys.count {
                return result
            } else {
                // get remain keyPath
                var nextKeys = [keys[keyIndex + 1]]
                var nextkeyPath = keys[keyIndex + 1]
                for i in 0..<keys.count {
                    if i > (keyIndex + 1) {
                        nextkeyPath += keys[i]
                        nextKeys.append(keys[i])
                    }
                }
                
                if propValue.isObjCRefType {
                    if let objcInstance = currInstance as? NSObject, let modelNode = self.findAstNode(by: propNode.declType!) {
                        return getValue(nextkeyPath, objcInstance, modelNode)
                    }
                } else {
                    return _getNativeStructValue(nextKeys, 0, result!, propNode.declType!)
                }
            }
        }
        return nil
    }
    
    fileprivate func _getNativeStructValue(_ keys: [String], _ keyIndex: Int, _ instance: Any, _ propType: String) -> Any? {
        let key = keys[keyIndex]
        if propType == "CGRect" || propType == "CGRect" {
            if let rect = instance as? CGRect {
                var result: Any! = nil
                var resultPropType: String! = nil
                if key == "size" {
                    result = rect.size
                    resultPropType = "CGSize"
                } else if key == "origin" {
                    result = rect.origin
                    resultPropType = "CGPoint"
                }
                if keyIndex + 1 == keys.count {
                    return result
                } else {
                    return _getNativeStructValue(keys, keyIndex + 1, result!, resultPropType!)
                }
            }
        } else if propType == "CGSize" || propType == "NSSize" {
            if let size = instance as? CGSize {
                if key == "width" {
                    return size.width
                } else if key == "height" {
                    return size.height
                }
            }
        } else if propType == "CGPoint" || propType == "NSPoint" {
            if let point = instance as? CGPoint {
                if key == "width" {
                    return point.x
                } else if key == "height" {
                    return point.y
                }
            }
        }
        return nil
    }
    
    func setValue(_ value: Any?, _ forKey: String, _ className: String, _ instance: AnyObject) {
        if let handler = self.handleCustomSetInstanceProperties, handler(className, instance, forKey, value) {
            return
        }
        
        var properties = [String : AstNode]()
        for node in self.astNodes[className]!.children where node.type == .property {
            properties[node.name] = node
        }
        if let prop = properties[forKey],
            let propValue = prop.value,
            let objCInstance = instance as? NSObject {
            var valueType: String = prop.declType!
            var objCValue: AnyObject!
            if var value = value {
                let enumType = String(reflecting: type(of: value))
                // convert Enum to Int
                if let rawType = propValue.rawType, rawType.count > 0 && enumType.starts(with: "__C.") {
                    value = castEnumToNativeValue(value: value, rawType: rawType)
                    valueType = rawType
                } else if let rawType = prop.declType, !propValue.isObjCRefType {
                    // handle enumValue to int/uint property
                    if enumType.starts(with: "__C.") {
                        let realType = enumType.replacingOccurrences(of: "__C.", with: "")
                        if !realType.contains(valueType), let enumNode = self.astNodes[realType], enumNode.type == .enumerate {
                            value = castEnumToNativeValue(value: value, rawType: rawType)
                        }
                    }
                }
                objCValue = castAnyToObjCValue(value: value,
                                               valueType: valueType,
                                               isObjCRefType: propValue.isObjCRefType)
            } else {
                objCValue = Optional<NSObject>.none as AnyObject
            }
            objCInstance.setValue(objCValue, forKey: forKey)
        }
    }
    
    // REMARK: invoke function on runtime
    
    @discardableResult
    func invoke(_ method: String, _ parameters: Any?...) -> Any? {
        return self.invoke(method, parameters: parameters)
    }
    
    @discardableResult
    func invoke(_ method: String, callAPICtx: [String : Any]=[:], parameters: [Any?]) -> Any? {
        let signals = checkAsyncParameter(parameters: parameters)
        if signals.count > 0 {
            combineLatest(signals)?.subscribeNext {[weak self] asyncParameters in
                if let self = self {
                    let syncParameters = self.getSyncParameters(origin: parameters, async: asyncParameters)
                    self._invoke(methods: self.apiMethods,
                                 targetClass: self.invokeTargetClass,
                                 target: self.invokeTarget ?? NSObject(),
                                 isStaticMethod: false,
                                 method: method,
                                 parameters: syncParameters,
                                 callAPICtx: callAPICtx,
                                 isSDKInstance: true)
                }
            }
            return nil
        } else {
            let (canResponds, result) = _invoke(methods: self.apiMethods,
                                                targetClass: self.invokeTargetClass,
                                                target: self.invokeTarget ?? NSObject(),
                                                isStaticMethod: false,
                                                method: method,
                                                parameters: parameters,
                                                callAPICtx: callAPICtx,
                                                isSDKInstance: true)
            if canResponds {
                return result
            } else {
                // TODO: dispatch not found method to log
                return nil
            }
        }
    }
    
    @discardableResult
    func invoke(instance: AnyObject,
                _ className: String,
                _ isStaticMethod: Bool,
                _ method: String,
                _ parameters: [Any?]) -> Any? {
        let (_, result) = _invoke(methods: self.apiMethods,
                                  targetClass: self.classesInfo[className]!,
                                  target: instance,
                                  isStaticMethod: isStaticMethod,
                                  method: method,
                                  parameters: parameters,
                                  callAPICtx: [:],
                                  isSDKInstance: self.invokeTargetClassName == className,
                                  instanceClass: className)
        return result
    }
    
    fileprivate func checkAsyncParameter(parameters: [Any?]) -> [IOMonad<Any>] {
        var result = [IOMonad<Any>]()
        for param in parameters {
            if let ioStream = param as? AnyIOStream {
                result.append(ioStream.mapAny())
            }
        }
        return result
    }

    fileprivate func getSyncParameters(origin: [Any?], async: [Any]) -> [Any?] {
        var parameters = origin
        var asyncPos = 0
        for i in 0..<origin.count {
            let param = origin[i]
            if param is AnyIOStream {
                let newParam = async[asyncPos]
                parameters[i] = newParam
                asyncPos += 1
            } else {
                parameters[i] = param
            }
        }
        return parameters
    }

    @discardableResult
    fileprivate func _invoke(methods: [String : AstNode],
                             targetClass: AnyClass,
                             target: AnyObject,
                             isStaticMethod: Bool,
                             method: String,
                             parameters: [Any?],
                             callAPICtx: [String : Any],
                             isSDKInstance: Bool,
                             instanceClass: String?=nil) -> (Bool, Any?) {
        if let node = methods[method], let typeEncoding = node.value?.typeEncoding {
            let objcTypes = NSString(string: typeEncoding).utf8String!
            let signature = Self.NSMethodSignatureClass.signature(objCTypes: objcTypes)
            let invocation = Self.NSInvocationClass.invocation(methodSignature: signature!)
            let isStatic = node.value?.isStaticValue ?? isStaticMethod
            invocation.target = isStatic ? targetClass as AnyObject : target
            invocation.selector = NSSelectorFromString(node.name)

            var argIndex = 2
            var blockArgIndex = 0
            var blockArgName: String? = nil
            var paramInfos: [ObjCInvokeParamInfo]? = nil
            var localCreateArgs: [Any?] = []
            var allArgs: [String : Any?] = [:]
            for param in node.children {
                if let paramValue = param.value {
                    // block params
                    if paramValue.isValueBlock, let blockSignature = paramValue.blockSignature {
                        blockArgIndex = argIndex
                        blockArgName = blockSignature.name
                        // test param is block type
                        if let _ = parameters[argIndex - 2] as? () {
                            let blockSelector = NSSelectorFromString(blockArgName!)
                            if blockMethods[blockSelector] == nil {
                                blockMethods[blockSelector] = blockSignature
                            }
                            
                            paramInfos = []
                            for blockParam in blockSignature.children {
                                let paramInfo = ObjCInvokeParamInfo()
                                paramInfo.name = blockParam.name
                                paramInfo.declType = blockParam.declType!
                                paramInfo.isObjCRefType = blockParam.value!.isObjCRefType
                                paramInfos?.append(paramInfo)
                            }
                        }
                    } else {
                        var arg: Any? = nil
                        if paramValue.isValueDelegate, let _ = parameters[argIndex - 2] as? () {
                            // set self as delegate param
                            arg = self.proxy
                        } else {
                            arg = parameters[argIndex - 2]
                        }
                        allArgs[param.name] = arg
                        if paramValue.isObjCRefType {
                            if param.declType == "NSString",
                                let stringVal = arg as? String,
                                // NSString.superclassMirror.subType == "NSMutableString"
                                Mirror(reflecting: stringVal).superclassMirror == nil {
                                var argString = NSString(string: stringVal)
                                invocation.setArgument(&argString, at: argIndex)
                                localCreateArgs.append(argString)
                            } else {
                                var argObj: NSObject? = arg as? NSObject
                                invocation.setArgument(&argObj, at: argIndex)
                            }
                        } else {
                            invocation.setArgument(&arg, at: argIndex)
                        }
                    }
                }
                argIndex += 1
            }
            self.proxy.invokeAPI(withInvocation: invocation,
                                 blockArgName: blockArgName,
                                 blockArgIndex: blockArgIndex,
                                 blockArgInfo: paramInfos)
            if localCreateArgs.count > 0 {
                localCreateArgs.removeAll()
            }
            return (true, self.dispatch(invocation,
                                        node: node,
                                        invokeArgs: allArgs,
                                        callAPICtx: callAPICtx,
                                        forCallback: false,
                                        forBlockValue: false,
                                        isSDKInstance: isSDKInstance,
                                        instanceClass: instanceClass))
        }
        // TODO: dispatch to logger
        return (false, nil)
    }
    
    // REMARK: response ObjC runtime methods
    
    lazy var proxy: ObjCInvokeProxy = {
        // proxy instance
        let proxy = ObjCInvokeProxy(canRespondBlock: {[weak self] selector -> Bool in
            if let self = self {
                return self.delegateSelectorMethods[selector] != nil || self.apiSelectorMethods[selector] != nil
            }
            return false
            }, searchSignatureBlock: {[weak self] selector -> String in
                if let typeEncoding = self?.delegateSelectorMethods[selector]?.value?.typeEncoding {
                    return typeEncoding
                } else if let typeEncoding = self?.apiSelectorMethods[selector]?.value?.typeEncoding {
                    return typeEncoding
                } else if let typeEncoding = self?.blockMethods[selector]?.value?.typeEncoding {
                    return typeEncoding
                }
                return ""
            }, forwardInvocationBlock: {[weak self] objcInvocation in
                if let self = self {
                    let invocation = objcInvocation as! NSInvocationPrivate
                    let delegateNode = self.delegateSelectorMethods[invocation.selector]
                    let apiNode = self.apiSelectorMethods[invocation.selector]
                    if let node = (delegateNode ?? apiNode) {
                        self.dispatch(invocation,
                                      node: node,
                                      callAPICtx: [:],
                                      forCallback:delegateNode != nil,
                                      forBlockValue: false,
                                      isSDKInstance: self.invokeTargetClassName.count > 0)
                    } else if let node = self.blockMethods[invocation.selector] {
                        self.dispatch(invocation,
                                      node: node,
                                      callAPICtx: [:],
                                      forCallback: true,
                                      forBlockValue: true,
                                      isSDKInstance: self.invokeTargetClassName.count > 0)
                    } else {
                        print(">>> occurs exception.")
                    }
                }
        })
        return proxy
    }()
    
    // REMARK: dispatch delegate、block
    
    @discardableResult
    fileprivate func dispatch(_ invocation: NSInvocationPrivate,
                              node: AstNode,
                              invokeArgs: [String : Any?]=[:],
                              callAPICtx: [String : Any],
                              forCallback: Bool,
                              forBlockValue: Bool,
                              isSDKInstance: Bool=true,
                              instanceClass: String?=nil) -> Any? {
        let engineName = self.engineName ?? ""
        let selector = node.name
        // default dispatch filter
        let defaultFilterFn: DispatchFilterFn = { (selector, forCallback) in
            let respondsFn: DispatchRespondsFn = { target in
                return (target as? ScriptEngineDispatch)
                    .map { (engineName,
                            $0,
                            selector,
                            forCallback, forBlockValue,
                            isSDKInstance,
                            instanceClass ?? self.invokeTargetClassName) }
                    .flatMap(defaultDispatchHandler)
            }
            return (true, respondsFn)
        }
        var returnValue: Any? = nil
        var argValues: [String : Any]! = nil
        let dispatchFilterFnList = [self.dispatchFilterFnList, [defaultFilterFn]].flatMap { $0 }
        var dispatchTargets: [AnyObject]! = nil
        let succ = mutex.tryLock()
        dispatchTargets = self.dispatchTargets
        if succ {
            mutex.unlock()
        }
        
        for target in dispatchTargets {
            var isContinue = true
            for filterFn in dispatchFilterFnList {
                var respondsFn: DispatchRespondsFn?
                (isContinue, respondsFn) = filterFn(selector, forCallback)
                if isContinue {
                    if let respondsFn = respondsFn, let handleFn = respondsFn(target) {
                        if argValues == nil {
                            if forCallback {
                                argValues = getArgValue(invocation: invocation,
                                                        method: node,
                                                        forReturnValue: false,
                                                        forBlockValue: forBlockValue)
                            } else {
                                argValues = getArgValue(invocation: invocation,
                                                        method: node,
                                                        forReturnValue: true,
                                                        forBlockValue: false)
                                returnValue = argValues.values.first!
                                if invokeArgs.count > 0 {
                                    argValues = [:]
                                    for (key, value) in invokeArgs {
                                        if let arg = value {
                                            argValues[key] = arg
                                        } else {
                                            argValues[key] = Optional<Any>.none as Any
                                        }
                                    }
                                    argValues["return"] = returnValue
                                    argValues["__ctx"] = callAPICtx
                                }
                            }
                        }
                        if self.runInMainQueueTargets.contains(target)
                            && !isRunInMainQueue() {
                            DispatchQueue.main.sync {
                                handleFn(argValues, node.value!.isStaticValue)
                            }
                        } else {
                            handleFn(argValues, node.value!.isStaticValue)
                        }
                    } else {
                        continue
                    }
                } else {
                    break
                }
            }
        }
        // get returnValue for targets not responds protocol
        if !forCallback && argValues == nil && returnValue == nil {
            argValues = getArgValue(invocation: invocation,
                                    method: node,
                                    forReturnValue: true,
                                    forBlockValue: false)
            returnValue = argValues.values.first!
        }
        return returnValue
    }
}

fileprivate func defaultDispatchHandler(engineName: String,
                                        target: ScriptEngineDispatch,
                                        selector: String,
                                        forCallback: Bool,
                                        forBlockValue: Bool,
                                        isSDKInstance: Bool,
                                        instanceClass: String) -> Action2<[String : Any], Bool> {
    return { (argValues, isStatic) in
        if forBlockValue {
            target.responseBlock(engineName: engineName,
                                 isStatic: isStatic,
                                 selector: selector,
                                 argValues: argValues,
                                 isSDKInstance: isSDKInstance,
                                 instanceClass: instanceClass)
        } else if forCallback {
            target.responseDelegate(engineName: engineName,
                                    isStatic: isStatic,
                                    selector: selector,
                                    argValues: argValues,
                                    isSDKInstance: isSDKInstance,
                                    instanceClass: instanceClass)
        } else {
            target.responseAPI(engineName: engineName,
                               isStatic: isStatic,
                               selector: selector,
                               argValues: argValues,
                               returnValue: argValues["return"]!,
                               isSDKInstance: isSDKInstance,
                               instanceClass: instanceClass)
        }
    }
}

// REMARK: help methods

extension ScriptEngine {
    func responds(_ methodName: String, className: String) -> Bool {
        if className.count > 0 {
            if let classNode = self.findAstNode(by: className) {
                for methodNode in classNode.children where methodNode.type == .method {
                    if methodNode.name == methodName {
                        return true
                    }
                }
            }
        } else {
            return self.findAPIMethodNode(by: methodName) != nil
        }
        return false
    }
    
    func getAllClassNodes() -> [AstNode] {
        var nodes = [AstNode]()
        for (_, node) in self.astNodes where node.type == .interface {
            nodes.append(node)
        }
        return nodes
    }
    
    func getAPIsMethods() -> [AstNode] {
        return Array(self.apiMethods.values)
    }
    
    func getDelegatesMethods() -> [AstNode] {
        return Array(self.delegateMethods.values)
    }
    
    func getEnumCaseValues() -> [AstNode] {
        var nodes = [AstNode]()
        for (_, node) in self.astNodes where node.type == .enumerate {
            for item in node.children where item.type == .property {
                nodes.append(item)
            }
        }
        return nodes
    }
    
    func findAstNode(by name: String) -> AstNode? {
        return self.astNodes[name]
    }
    
    func findAPIMethodNode(by selector: String) -> AstNode? {
        return self.apiMethods[selector]
    }
    
    func findDelegateMethodNode(by selector: String) -> AstNode? {
        return self.delegateMethods[selector]
    }
    
    // TODO: remove
    
    func checkParamsAvailable(for methodNode: AstNode, params: [Any?]?) -> Bool {
        if methodNode.type == .method {
            if methodNode.children.count > 0 {
                if let params = params, params.count == methodNode.children.count {
                    // check nullable
                    for i in 0..<methodNode.children.count {
                        let param = methodNode.children[i]
                        let value = params[i]
                        if param.value!.isValueNonnull && value == nil {
                            return false
                        }
                    }
                    // TODO: type check
                    return true
                }
            } else {
                return true
            }
        }
        return false
    }
    
    fileprivate static func setOrder(node: AstNode) {
        if node.type == .interface || node.type == .protocol
            || node.type == .enumerate || node.type == .method {
            var order = 1
            for item in node.children {
                item.order = order
                if item.type == .method {
                    setOrder(node: item)
                }
                order += 1
            }
        }
    }
}
